<!--
 * @use: 
 * @description: 布局面板
 * @Author: vision
 * @Date: 2023-09-07 11:25:31
 * @FilePath: /shared/modules/src/components/v-pane/src/v-split-pane.vue
-->
<template>
  <div
    ref="containerRef"
    v-size-change="resetPaneSizes"
    :class="[
      'v-split-pane',
      `v-split-pane--${props.horizontal ? 'horizontal' : 'vertical'}`,
      {
        'v-split-pane--dragging': state.touch.dragging,
      },
    ]"
  >
    <slot></slot>
  </div>
</template>
<script setup lang="ts">
  import { ref, reactive, provide, onMounted, onBeforeUnmount, computed, watch, nextTick } from 'vue'

  import { tool } from '@v-model/shared--utils'
  import { vSizeChange } from '@v-model/shared--directives'

  defineOptions({ name: 'v-split-pane' })
  interface splitpaneProps {
    horizontal?: boolean
    pushOtherPanes?: boolean
    dblClickSplitter?: boolean
    splitterColor?: string
    activeColor?: string
    splitterSize?: string
  }
  /*--------------------------------types--------------------------------*/
  import {
    // splitpaneProps,
    KeyValue,
    pane,
    paneSize,
    splitpaneState,
    RequestUpdateArgs,
    requestUpdate,
    onPaneAdd,
    onPaneRemove,
    onPaneClick,
    requestUpdate_key,
    onPaneRemove_key,
    onPaneAdd_key,
    onPaneClick_key,
  } from './type'

  /*--------------------------------utils--------------------------------*/
  import { formatSize } from './util'

  /*--------------------------------props--------------------------------*/

  const props = withDefaults(defineProps<splitpaneProps>(), {
    horizontal: false,
    pushOtherPanes: false,
    dblClickSplitter: true,
    splitterColor: () => '#dcdfe6',
    activeColor: () => '#c8c9cc',
    splitterSize: () => '1px',
  })

  /*--------------------------------state--------------------------------*/
  const containerRef = ref<HTMLElement | null>(null)

  const state = reactive(<splitpaneState>{
    uid: tool.guid(),
    horizontal: props.horizontal,
    container: containerRef.value,
    ready: false,
    panes: [],
    touch: {
      mouseDown: false,
      dragging: false,
      activeSplitter: null,
    },
    splitterTaps: {
      splitter: null,
      timeoutId: null,
    },
    splitterColor: props.splitterColor,
    activeColor: props.activeColor,
    splitterSize: props.splitterSize,
  })

  /*--------------------------------computed--------------------------------*/
  const panesCount = computed(() => {
    return state.panes.length
  })

  const indexedPanes = computed(() => {
    return state.panes.reduce((obj: any, pane: pane) => (obj[pane.uid] = pane) && obj, {})
  })
  /*--------------------------------watch--------------------------------*/
  watch(
    () => state.panes,
    () => {
      updatePaneComponents()
    },
    { deep: true }
  )
  watch(
    () => props.horizontal,
    () => {
      updatePaneComponents()
    }
  )

  watch(
    () => props.dblClickSplitter,
    (enable) => {
      if (state.container) {
        const splitters = [...state.container.querySelectorAll('.v-split-pane__splitter')]
        splitters.forEach((splitter: Element, i: number) => {
          const __splitter = <HTMLElement>splitter
          //((this: GlobalEventHandlers, ev: MouseEvent) => any) | null
          __splitter.ondblclick = enable ? (event: Event) => onSplitterDblClick(event, i) : null
        })
      }
    }
  )

  /*--------------------------------method--------------------------------*/

  const updatePaneComponents = () => {
    state.panes.forEach((pane: pane) => {
      //console.log(pane.uid,pane.size)
      //console.log(indexedPanes.value)
      //调用子组件方法
      pane.update({
        [state.horizontal ? 'height' : 'width']: `${indexedPanes.value[pane.uid].size}`,
      })
    })
  }
  //获取当前组件大小
  const getClientSize = (): number => {
    let size = 0
    if (state.container) {
      state.horizontal ? (size = state.container.clientHeight) : (size = state.container.clientWidth)
    }
    return size
  }
  //转换为绝对大小数字
  const convert2abs = (paneSize: paneSize): number => {
    let _abs = 0
    if (paneSize.unit == 'px') {
      _abs = paneSize.value
    } else {
      let container_abs = getClientSize()
      if (container_abs) {
        _abs = (paneSize.value / 100) * container_abs
      }
    }
    return _abs
  }
  const getAbsSize = (sizeStr: string): number => {
    const sizeObj = formatSize(sizeStr)
    return convert2abs(sizeObj)
  }
  //绝对大小根据size设置还原
  const restoreSize = (absSize: number, unit: string): string => {
    let size = ''
    if (unit == 'px') {
      size = absSize + 'px'
    } else {
      const ClientSize = getClientSize()
      size = (Math.floor((absSize / ClientSize) * 1000000) / 1000000) * 100 + '%'
    }
    return size
  }
  //获取鼠标相对于容器的位置
  const getMouseCoords = (event: Event): KeyValue => {
    let rect: any = { left: 0, top: 0 }
    rect = state.container?.getBoundingClientRect()
    const evt = event as TouchEvent
    const { clientX, clientY } = 'ontouchstart' in window && evt.touches ? evt.touches[0] : (event as MouseEvent)
    return {
      x: clientX - rect.left,
      y: clientY - rect.top,
    }
  }
  function onMouseMove(event: Event): any {
    if (state.touch.mouseDown) {
      event.preventDefault()
      state.touch.dragging = true
      const coords = getMouseCoords(event)
      calPanesSize(coords)
      emits(
        'resize',
        state.panes.map((pane: pane) => ({ min: pane.min, max: pane.max, size: pane.size }))
      )
    }
  }
  function onMouseUp(): any {
    if (state.touch.dragging) {
      emits(
        'resized',
        state.panes.map((pane: pane) => ({ min: pane.min, max: pane.max, size: pane.size }))
      )
    }
    state.touch.mouseDown = false
    setTimeout(() => {
      state.touch.dragging = false
      unbindEvents()
    }, 100)
  }

  function bindEvents() {
    //onMouseMove 注意不能使用箭头函数
    //如果你在添加事件监听器时使用了一个匿名函数或者箭头函数，那么将无法通过 `removeEventListener` 来移除它，
    //因为 `removeEventListener` 需要传入相同的引用才能正确移除。
    document.addEventListener('mousemove', onMouseMove, { passive: false })
    document.addEventListener('mouseup', onMouseUp)
    if ('ontouchstart' in window) {
      document.addEventListener('touchmove', onMouseMove, { passive: false })
      document.addEventListener('touchend', onMouseUp)
    }
  }

  function unbindEvents() {
    //document.addEventListener('mousemove', onMouseMove, { passive: false });
    //document.removeEventListener('mousemove', onMouseMove as unknown as (this: Document, ev:MouseEvent) => any);
    document.removeEventListener('mousemove', onMouseMove)
    document.removeEventListener('mouseup', onMouseUp)

    if ('ontouchstart' in window) {
      document.removeEventListener('touchmove', onMouseMove)
      document.removeEventListener('touchend', onMouseUp)
    }
  }
  function onMouseDown(event: Event, splitterIndex: any) {
    bindEvents()
    state.touch.mouseDown = true
    state.touch.activeSplitter = splitterIndex
  }

  const onSplitterClick = (event: Event, splitterIndex: any) => {
    if ('ontouchstart' in window) {
      event.preventDefault()
      if (props.dblClickSplitter) {
        if (state.splitterTaps.splitter === splitterIndex) {
          clearTimeout(state.splitterTaps.timeoutId)
          state.splitterTaps.timeoutId = null
          onSplitterDblClick(event, splitterIndex)
          state.splitterTaps.splitter = null
        } else {
          state.splitterTaps.splitter = splitterIndex
          state.splitterTaps.timeoutId = setTimeout(() => {
            state.splitterTaps.splitter = null
          }, 500)
        }
      }
    }
    if (!state.touch.dragging) emits('splitter-click', state.panes[splitterIndex])
  }

  const onSplitterDblClick = (event: Event, splitterIndex: any) => {
    //使前一个pane最小值和初始值切换
    const pane = state.panes[splitterIndex]
    const _pane_min_abs = getAbsSize(pane.min)
    const _pane_size_abs = getAbsSize(pane.size)
    const _pane_givesize_abs = getAbsSize(pane.givenSize || pane.size)

    //console.log(pane);

    if (_pane_size_abs > _pane_min_abs) {
      //缩小
      pane.size = restoreSize(_pane_min_abs, formatSize(pane.size).unit)
    } else {
      //还原
      pane.size = restoreSize(_pane_givesize_abs, formatSize(pane.size).unit)
    }
    resetPaneSizes()
  }

  const onPaneClick = (event: Event, uid: string) => {
    emits('pane-click', indexedPanes.value[uid])
  }

  //在容器内移动的绝对大小
  const getCurrentDragAbs = (coords: KeyValue) => {
    //coor={x:left,y:top}
    const _drag_abs = coords[state.horizontal ? 'y' : 'x']
    return _drag_abs
  }

  const getAllSplitterSize = (): number => {
    return getAbsSize(state.splitterSize) * (state.panes.length - 1)
  }
  function calPanesSize(coords: KeyValue) {
    // drag={x:left,y:top}
    const splitterIndex = state.touch.activeSplitter
    let sums = {
      prevPanesSize: sumPrevPanesSize(splitterIndex), //之前所有pane大小
      nextPanesSize: sumNextPanesSize(splitterIndex), //之后所有pane大小
      prevReachedMinPanes: 0,
      nextReachedMinPanes: 0,
    }
    let allocate_abs = getClientSize() - getAllSplitterSize()

    //相邻的两个pane
    let panesToResize = [splitterIndex, splitterIndex + 1]
    let paneBefore: pane = state.panes[panesToResize[0]] || null
    let paneAfter: pane = state.panes[panesToResize[1]] || null

    //鼠标相对于容器的绝对位置
    const dragAbs = getCurrentDragAbs(coords) //Math.max(Math.min(getCurrentDragAbs(coords), maxDrag), minDrag)

    if (
      dragAbs >= sums.prevPanesSize + getAbsSize(paneBefore.min) &&
      dragAbs <= sums.prevPanesSize + getAbsSize(paneBefore.max) &&
      dragAbs <= allocate_abs - sums.nextPanesSize - getAbsSize(paneAfter.min)
    ) {
      if (paneBefore !== null) {
        const new_size = Math.min(Math.max(dragAbs - sums.prevPanesSize, getAbsSize(paneBefore.min)), getAbsSize(paneBefore.max))
        paneBefore.size = restoreSize(new_size, formatSize(paneBefore.size).unit)
      }

      if (paneAfter !== null) {
        const new_size = Math.min(
          Math.max(allocate_abs - sums.nextPanesSize - dragAbs, getAbsSize(paneAfter.min)),
          getAbsSize(paneAfter.max)
        )
        paneAfter.size = restoreSize(new_size, formatSize(paneAfter.size).unit)
      }
    }
  }

  const sumPrevPanesSize = (splitterIndex: number) => {
    return state.panes.reduce(
      (total: number, pane: pane, i: number) => total + (i < splitterIndex ? getAbsSize(pane.size) : 0),
      0
    )
  }

  const sumNextPanesSize = (splitterIndex: number) => {
    return state.panes.reduce(
      (total: number, pane: pane, i: number) => total + (i > splitterIndex + 1 ? getAbsSize(pane.size) : 0),
      0
    )
  }

  const checkChildNodes = () => {
    let children: HTMLElement[] = []
    if (state.container) {
      children = Array.from(state.container.children) as HTMLElement[]
    }
    children.forEach((child: HTMLElement) => {
      const isPane = child.classList.contains('v-split-pane__pane')
      const isSplitter = child.classList.contains('v-split-pane__splitter')
      if (!isPane && !isSplitter && child.parentNode) {
        child.parentNode.removeChild(child)
        console.warn(
          'v-split-pane: Only <v-pane> elements are allowed at the root of <v-split-pane>. One of your DOM nodes was removed.'
        )
      }
    })
  }

  const addSplitter = (paneIndex: number, nextPaneNode: HTMLElement) => {
    const splitterIndex = paneIndex - 1
    const elm = document.createElement('div')
    elm.classList.add('v-split-pane__splitter')

    elm.onmousedown = function (event) {
      onMouseDown(event, splitterIndex)
    }
    if (typeof window !== 'undefined' && 'ontouchstart' in window) {
      elm.ontouchstart = function (event) {
        onMouseDown(event, splitterIndex)
      }
    }
    elm.onclick = function (event) {
      onSplitterClick(event, splitterIndex)
    }

    if (props.dblClickSplitter) {
      elm.ondblclick = function (event) {
        onSplitterDblClick(event, splitterIndex)
      }
    }
    nextPaneNode?.parentNode?.insertBefore(elm, nextPaneNode)
  }

  const removeSplitter = (__node: HTMLElement) => {
    //const __node = <HTMLElement>node;
    __node.onmousedown = null
    __node.onclick = null
    __node.ondblclick = null
    __node?.parentNode?.removeChild(__node) // el.remove() doesn't work on IE11.
  }

  const redoSplitters = () => {
    let children: HTMLElement[] = []
    if (state.container) {
      children = Array.from(state.container.children) as HTMLElement[]
    }
    children.forEach((el) => {
      if (el.className.includes('v-split-pane__splitter')) removeSplitter(el)
    })

    let paneIndex = 0
    children.forEach((el) => {
      if (el.className.includes('v-split-pane__pane')) {
        if (paneIndex > 0) {
          addSplitter(paneIndex, el)
        }
        paneIndex++
      }
    })
  }
  const requestUpdate = ({ uid, ...args }: RequestUpdateArgs): void => {
    const pane: pane = indexedPanes.value[uid]

    if (!pane || !args) return
    Object.entries(args).forEach(([key, value]) => {
      pane[key] = value
    })
    resetPaneSizes()
  }

  const onPaneAdd = (pane: pane): void => {
    let index = -1
    Array.from(pane.el.parentNode.children).some((el: any) => {
      if (el.className.includes('v-split-pane__pane')) index++
      return el === pane.el
    })
    const _pane = {
      index: index,
      uid: pane.uid,
      min: !pane.minSize ? '0%' : pane.minSize,
      max: !pane.maxSize ? '100%' : pane.maxSize,
      size: pane.size === null ? null : pane.size,
      givenSize: pane.size, //初始大小
      update: pane.update,
    }
    state.panes.splice(index, 0, _pane)

    state.panes.forEach((p: pane, i: number) => (p.index = i))

    if (state.ready) {
      nextTick(() => {
        // 2. Add the splitter.
        redoSplitters()
        // 3. Resize the panes.
        console.log('Resize the panes')
        resetPaneSizes()
        // 4. Fire `pane-add` event.
        emits('pane-add', {
          index,
          panes: state.panes.map((pane: pane) => ({
            min: pane.min,
            max: pane.max,
            size: pane.size,
          })),
        })
      })
    }
  }
  const onPaneRemove = (pane: pane): void => {
    // 1. Remove the pane from array and redo indexes.
    const index = state.panes.findIndex((p: pane) => p.uid === pane.uid)
    const removed = state.panes.splice(index, 1)[0]
    state.panes.forEach((p: pane, i: number) => (p.index = i))
    nextTick(() => {
      // 2. Remove the splitter.
      redoSplitters()
      // 3. Resize the panes.
      resetPaneSizes()
      // 4. Fire `pane-remove` event.
      emits('pane-remove', {
        removed,
        panes: state.panes.map((pane: pane) => ({ min: pane.min, max: pane.max, size: pane.size })),
      })
    })
  }

  const resetPaneSizes = () => {
    // initPanesSize()
    if (state.ready) {
      emits(
        'resized',
        state.panes.map((pane: pane) => ({ min: pane.min, max: pane.max, size: pane.size }))
      )
    }
  }

  //初始化panes大小
  const initPanesSize = () => {
    //剩余绝对大小
    let leftToAllocate_abs = getClientSize()
    let definedSizes = 0
    //减去splitter
    leftToAllocate_abs -= getAllSplitterSize()

    //重新计算每个pane的大小,防止自动压缩
    state.panes.forEach((pane: pane) => {
      if (pane.givenSize) {
        const _pane_size_abs = getAbsSize(pane.size)
        if (leftToAllocate_abs > 0) {
          if (leftToAllocate_abs >= _pane_size_abs) {
            leftToAllocate_abs -= _pane_size_abs
          } else {
            //改变size为leftToAllocate_abs
            pane.size = restoreSize(leftToAllocate_abs, formatSize(pane.size).unit)
            leftToAllocate_abs -= _pane_size_abs
          }
        } else {
          //改变size为0
          pane.size = restoreSize(0, formatSize(pane.size).unit)
        }
        definedSizes++
      }
    })

    //未设置大小的pane
    if (leftToAllocate_abs > 0) {
      state.panes.forEach((pane: pane) => {
        //初始大小使用 givenSize 判断
        if (pane.givenSize === null) {
          //未定义size的平分
          const _pane_max_abs = getAbsSize(pane.max)
          const _pane_min_abs = getAbsSize(pane.min)
          const _pane_size_abs = Math.max(
            Math.min(leftToAllocate_abs / (panesCount.value - definedSizes), _pane_max_abs),
            _pane_min_abs
          )
          pane.size = restoreSize(_pane_size_abs, '%')
        }
      })
    }
  }

  /*--------------------------------defineEmits--------------------------------*/
  const emits = defineEmits([
    'ready',
    'resize',
    'resized',
    'pane-click',
    'pane-maximize',
    'pane-add',
    'pane-remove',
    'splitter-click',
  ])

  /*--------------------------------provide--------------------------------*/

  provide<requestUpdate>(requestUpdate_key, requestUpdate)

  provide<onPaneAdd>(onPaneAdd_key, onPaneAdd)

  provide<onPaneRemove>(onPaneRemove_key, onPaneRemove)

  provide<onPaneClick>(onPaneClick_key, onPaneClick)

  /*--------------------------------hooks--------------------------------*/
  onMounted(() => {
    state.container = containerRef.value
    checkChildNodes()
    redoSplitters()
    initPanesSize()
    emits('ready')
    state.ready = true
  })
  onBeforeUnmount(() => {
    state.ready = false
  })
</script>

<style lang="scss">
  .v-split-pane {
    display: flex;
    width: 100%;
    height: 100%;
    overflow: hidden;

    &--vertical {
      flex-direction: row;
    }
    &--horizontal {
      flex-direction: column;
    }
    &--dragging * {
      user-select: none;
    }

    &__pane {
      width: 100%;
      height: 100%;
      overflow: hidden;

      .v-split-pane--vertical & {
        transition: width 0.2s ease-out;
      }
      .v-split-pane--horizontal & {
        transition: height 0.2s ease-out;
      }
      .v-split-pane--dragging & {
        transition: none;
      }
    }
    &__splitter {
      touch-action: none;
    }
    &--vertical > .v-split-pane__splitter {
      min-width: v-bind('state.splitterSize');
      cursor: col-resize;
    }
    &--horizontal > .v-split-pane__splitter {
      min-height: v-bind('state.splitterSize');
      cursor: row-resize;
    }
  }

  .v-split-pane__splitter {
    background-color: v-bind('state.splitterColor');
    position: relative;
  }
  .v-split-pane__splitter:before {
    content: '';
    position: absolute;
    top: 0; //calc(50% - 20px);
    left: 0; //calc(50% - 20px);
    transition: opacity 0.4s;
    background-color: v-bind('state.activeColor');
    opacity: 0;
    z-index: 1;
    //cursor: pointer;
  }
  .v-split-pane__splitter:hover:before {
    opacity: 0.4;
  }
  .v-split-pane--vertical > .v-split-pane__splitter:before {
    left: -4px;
    right: -4px;
    height: 100%;
    //height: 40px;
  }
  .v-split-pane--horizontal > .v-split-pane__splitter:before {
    top: -4px;
    bottom: -4px;
    width: 100%;
    //width: 40px;
  }
</style>
